-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix #1519: Port compiler Plugin from scalac #3438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
def test(tpe: String): Boolean = | ||
(sym.owner eq ctx.requiredClass(tpe.toTermName)) && sym.name.show == "/" | ||
|
||
test("scala.Int") || test("scala.Long") || test("scala.Short") || test("scala.FLoat") || test("scala.Double") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: FLoat -> Float
|
||
private def isNumericDivide(sym: Symbol)(implicit ctx: Context): Boolean = { | ||
def test(tpe: String): Boolean = | ||
(sym.owner eq ctx.requiredClass(tpe.toTermName)) && sym.name.show == "/" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to do sym.name == nme.DIV
@@ -0,0 +1,4 @@ | |||
<plugin> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should check that sbt's support for compiler plugin still works and adapt it in sbt-dotty if it doesn't: http://www.scala-sbt.org/1.x/docs/Compiler-Plugins.html We can do that by adding scripted tests in https://github.com/lampepfl/dotty/tree/master/sbt-dotty/sbt-test/sbt-dotty
val phaseName = name | ||
|
||
override def init(phases: List[List[Phase]])(implicit ctx: Context): List[List[Phase]] = { | ||
val (before, after) = phases.span(ps => !ps.exists(_.phaseName == "pickler")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to not rely on strings to identify phase names and use class types instead, similar to what we do with Phase.replace
in https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/repl/ReplCompiler.scala#L40
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strings compose with phases coming from other compiler plugins. Class types do not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sjrd Good point. But can't we use Class.forName
in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If all compiler plugins are loaded in the same ClassLoader
, then yes. But is that the case and is it desirable?
The interface with override def init(phases: List[List[Phase]])(implicit ctx: Context): List[List[Phase]] = {
val (before, after) = phases.span(ps => !ps.exists(_.phaseName == "pickler"))
before ++ (List(this) :: after)
} First, it is too powerful, because it allows to reorder core phases, or phases of other plugins. Also, it allows to insert micro-phases in existing macro-phases, which is extremely sensitive. I do not believe anyone outside of the core compiler team can write a correct micro-phase that would interact badly with the rest of its macro phase. IMO compiler plugins should only be able to insert macro phases (themselves possibly consisting of multiple micro-phases, all provided by the same plugin). Then, it is not powerful enough, because it does not allow to correctly express dependencies between compiler plugins. With the scalac compiler plugin interface, I can write a compiler plugin B that knows about another compiler plugin A, and correctly declares its dependencies wrt. that one. With this interface, this is not possible. Let me illustrate this with a very concrete (and existing!) example: The Scala.js JUnit compiler plugin has a phase This means that obtaining correct inter-plugin dependencies depends on the order in which the For these reasons, I disapprove of this new interface. I strongly suggest an interface that is closer to scalac, specifying a list of |
@sjrd Thanks a lot for sharing your experience, it's a valuable feedback 👍 We are in agreement that the new design is more powerful. As compiler plugins are for very advanced users & researchers, we may assume they use the power wisely. I hope you can also agree that the protocol/API is simpler, and the implementation is easy. In general, the usability for plugin writers is better. For the particular problem you raised, is it possible for the The problem is in general about composability of plugins, which we know is very tricky: compiler plugins just don't compose easily. The Scalac approach solves some of the problems by allowing plugins to specify ordering constraints. But the solution in Scalac is still unsatisfactory. How can a plugin knows in advance its relative order to all other plugins, given that
The new approach instead shifts all the responsibility to programmers. This seems to be a more coherent approach to me. In practice, a few plugins will be regularly used together to solve a particular problem. Due to the frequency of the combined usage, frictions among the plugins will be detected and reduced, compatibility will improve and the relative ordering will be known. Then, it's simple to just create an orchestrate/facade plugin to compose the phase plan if the ordering is sensitive. In the new approach, creating a facade compiler plugin is easy, just a few lines of code will do. As compiler plugins usually work with sbt, the compiler plugin facade can be authored in SBT( From the viewpoint of design, if practical composability problems can solved with the compiler taking ZERO responsibility, then it doesn't make sense for the compiler to take just PARTIAL responsibility. I assume It's always better for the boundary of responsibilities to be clear. |
@@ -87,7 +89,7 @@ class PluginsTest { | |||
def orderingTwoPlugins1 = { | |||
object M1 extends TestPhase { | |||
override val runsAfter = Set(classOf[P3d]) | |||
override val runsBefore = Set(M2.getClass, classOf[P7], classOf[P8]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opened #3495 to keep track of this.
Something to keep in mind: the dynamic classloading of compiler plugins seems to have a significant performance cost, which could perhaps be avoided by better caching of classloaders, see scala/scala-dev#458 |
@liufengyun what's is the status of this PR? If it needs a review please assign it to a reviewer :) |
@OlivierBlanvillain This is up for review. I think @sjrd has the best experience on plugins, but it's good to get more reviews. |
Unfortunately I won't have time to review this myself in the near future. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I finally got around to reviewing this. Thanks @smarter for the ping.
* Research plugin receives a phase plan and return a new phase plan, while | ||
* non-research plugin returns a list of phases to be inserted. | ||
*/ | ||
def research: Boolean = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A boolean flag, and then two methods among which only one should be overridden, can be advantageously replaced by:
- Making
Plugin
sealed
, with neither of those three methods - Have 2 subtraits
ResearchPlugin
andStandardPlugin
, each only with the one relevant abstract method.
/** ... | ||
* | ||
* @author Lex Spoon | ||
* @version 1.0, 2007-5-21 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this relevant?
*/ | ||
object Plugin { | ||
|
||
private val PluginXML = "scalac-plugin.xml" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be dotc-plugin.xml
. Plugins for one or the other compiler are definitely not interoperable, so they should be identified by a different file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, the "name" in that xml is unused. Maybe it's possible to ditch the xml. See Adriaan's work to avoid dependency on scala-xml at some point. It could be a properties file, or just a file containing a class name, radical simplicity. More like java's service loader. Seems a shame to drag xml config into scala 3.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, only the class name is actually used. Maybe we can assume a text file named plugins
with each line corresponding to a class name.
Success[AnyClass](loader loadClass classname) | ||
} catch { | ||
case NonFatal(e) => | ||
Failure(new PluginLoadException(classname, s"Error: unable to load class: $classname")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't e.toString
be included in the PluginLoadException
? Currently this completely discards all the relevant information that could be used to debug an issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oddly, I recall inheriting that when I touched the code. Generally e.getMessage == classname
for ClassNotFoundException
, but this code could be more nuanced in avoiding repeating the name, especially for other throwables or if there's a cause. I must have added the next line in this vein. You're more likely to hit NoClassDef
with multiple class loaders?
} | ||
|
||
private def loadDescriptionFromFile(f: Path): Try[PluginDescription] = | ||
Try(PluginDescription.fromXML(new java.io.FileInputStream(f.jpath.toFile))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From an API point of view, I think it would be more consistent to call this method loadDescriptionFromDirectory
, and have it take the directory containing the PluginXML
file (rather than the PluginXML
file). This is more consistent wrt. loadDescriptionFromJar
, because on a classpath, a jar ~= a directory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not API, but it would be more consistent. Both methods could be local.
import scala.util.{ Try, Success, Failure } | ||
|
||
trait PluginPhase extends MiniPhase { | ||
def runsBefore: Set[Class[_ <: Phase]] = Set.empty |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For inter-plugin dependencies, it will be impossible for a plugin A to get hold of the Class[_]
value for a plugin B coming from a different jar (hence in a different ClassLoader
, so not even reflectively available).
We need to use names here (also for runsAfter
). Names are usually not as good as token identities, but here they offer an essential part of the expressiveness.
def test(tpe: String): Boolean = | ||
(sym.owner eq ctx.requiredClass(tpe.toTermName)) && sym.name.show == "/" | ||
|
||
test("scala.Int") || test("scala.Long") || test("scala.Short") || test("scala.FLoat") || test("scala.Double") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: FLoat
-> Float
@@ -0,0 +1 @@ | |||
sbt.version=0.13.15 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0.13.17?
def test(tpe: String): Boolean = | ||
(sym.owner eq ctx.requiredClass(tpe.toTermName)) && sym.name.show == "/" | ||
|
||
test("scala.Int") || test("scala.Long") || test("scala.Short") || test("scala.FLoat") || test("scala.Double") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same typo on FLoat
def test(tpe: String): Boolean = | ||
(sym.owner eq ctx.requiredClass(tpe.toTermName)) && sym.name.show == "/" | ||
|
||
test("scala.Int") || test("scala.Long") || test("scala.Short") || test("scala.FLoat") || test("scala.Double") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And again the same typo.
Thanks a lot for the review @sjrd , I'll address them this weekend. |
Plugin.load(pd.classname, loader) | ||
case Failure(e) => | ||
Failure(e) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try.flatMap
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original use of map
is a good fit here. We are changing Success
to Failure
in the first two cases, Try.flatMap
will not help here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try.flatMap allows changing Success
to Failure
:
(fromPaths ::: fromDirs) flatMap {
case (pd, loader) if seen(pd.classname) => Failure(new PluginLoadException(pd.name, s"Ignoring duplicate plugin ${pd.name} (${pd.classname})"))
case (pd, loader) if ignoring contains pd.name =>
Failure(new PluginLoadException(pd.name, s"Disabling plugin ${pd.name}"))
case (pd, loader) =>
seen += pd.classname
Plugin.load(pd.classname, loader)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @Blaisorblade , sure, that's the whole purpose of flatMap/bind, a temporary short-circuit in my brain ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now to backport to scala 2.
Since this PR ports existing code, it would be good to add a note to https://github.com/lampepfl/dotty/blob/master/AUTHORS.md for proper attribution. |
Quote @sjrd: For inter-plugin dependencies, it will be impossible for a plugin A to get hold of the `Class[_]` value for a plugin B coming from a different jar (hence in a different `ClassLoader`, so not even reflectively available). scala#3438 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice :) Looks good to me :)
I would have smiley-faced sjrd's approval comment if github provided that function. |
Fix #1519: This PR ports compiler plugin from Scalac and adds the test infrastructure.
Scalac also supports Typer plugins, which is contentious, so it's not addressed in this PR. Current design allows easy extension to support Typer plugins if we want to do that later.
Changes from Scalac plugin:
PIuginComponent
TODOs: